const util = require('util');
const conf = require('./config.json');
const js = require('./jsCtl');

const co = require('co');

/** 全进程对象内存缓存
 *  set设置,get获取,destroy删除
 *  简单易用好上手
 */
const store = {
    storage: {},
    storage_timeout: {},
    /** 保存缓存
     * 
     * @param {Number} [timeout] 缓存过期时间,单位秒
     */
    set(key, val, timeout = conf.memory_cache_outtime) {
        this.storage[key] = val;
        if (timeout <= 0) timeout = 0;
        else timeout = Date.parse(new Date()) + timeout * 1000;
        this.storage_timeout[key] = timeout;
    },
    /** 删除缓存 */
    destroy(key) {
        delete this.storage[key];
        delete this.storage_timeout[key];
    },
    /** 获取缓存 */
    get(key) {
        if (!this.storage[key] || !this.storage_timeout[key]) {
            this.destroy(key);
            return null;
        }
        if (this.storage_timeout[key] != 0 && this.storage_timeout[key] < Date.parse(new Date())) {
            this.destroy(key);
            return null;
        }
        return this.storage[key];
    }
};
//jwt专用缓存对象
const jwt_store = js.cloneObject(store);

/** 将 async 异步函数（或者一个返回值为 Promise 的函数）转换成传统回调函数
 * 
 * @summary 例如将 (err, value) => ... 回调作为最后一个参数。 在回调函数中，第一个参数为拒绝的原因（如果 Promise 解决，则为 null），第二个参数则是解决的值。
 * 
 * @param {Function} fn 待转换的函数
 * @returns {Function} 传统回调函数
 * @example 
    async function fn() {
        return 'hello world'
    }
    const cbFunction = cc.callbackify(fn)

    cbFunction((err, ret) => {
        if (err) throw err
        console.log(ret)
    })
 */
const callbackify = (fn) => {
    return util.callbackify(fn);
};

/** 将传统回调函数转换为Promise函数
 * 
 * @summary 传入一个遵循常见的错误优先的回调风格的函数（即以 (err, value) => ... 回调作为最后一个参数），并返回一个返回 promise 的版本。
 * @param {Function} fn 待转换的函数
 * @returns {Promise} Promise函数
 * @example
 * 使用util.promisify.custom方式指定重写返回, 可以让任意函数转换为Promise函数:
 * 使用一个接受 (foo, onSuccessCallback, onErrorCallback) 的函数:
    doSomething[util.promisify.custom] = (foo) => {
        return new Promise((resolve, reject) => {
            doSomething(foo, resolve, reject)
        })
    }
 */
const promisify = (fn) => {
    return util.promisify(fn);
};

/** 返回系统错误名称
 * 
 * @param {Error} err 错误内容
 */
const getSystemErrorName = (err) => {
    let fmterr;
    try {
        fmterr = util.getSystemErrorName(err);
    } catch (err2) {
        fmterr = err.message || 'unknown error';
    }
    return fmterr;
};

/** 将对象输出到控制台
 * 
 * @summary 可配合nodejs-forever, 将输出到文件
 * @listens conf.isdebug=true
 * @param {*} obj 要记输出的主要对象
 * @param  {...any} therm 要输出的其它对象
 */
const log = (obj, ...therm) => {
    if (!conf.isdebug) return;
    let time = js.format_datetime(new Date(), 'yyyy-MM-dd HH:mm:ss:SS');
    let prints1 = util.inspect(obj, false, null, false);
    if (!therm.length) {
        console.log(time, prints1);
        return;
    }
    let prints2 = util.inspect(therm, false, null, false);
    console.log(time, prints1, prints2);
};

/** 将错误输出到控制台
 * 
 * @summary 可配合nodejs-forever, 将输出到文件
 * @param {*} errObj 主要错误内容
 * @param {int} errId 错误ID,非必填,默认js.rescode.undefined_fail
 * @param  {...any} therm 其它附加内容
 * @example
 * cc.err(js.rescode.net_fail, err, body)
 * cc.err(err)
 */
const err = (errObj, errId, ...therm) => {
    if (errId && typeof errId !== 'number') {
        if (therm) therm.unshift(errId);
        else therm = [errId];
        errId = js.rescode.undefined_fail;
    }
    let time = js.format_datetime(new Date(), 'yyyy-MM-dd HH:mm:ss:SS');
    let prints1 = util.types.isNativeError(errObj) ? util.inspect(errObj, false, 1, false) : util.inspect(errObj, false, null, false);
    if (!therm.length) {
        console.error(time, errId, prints1);
        return true;
    }
    let prints2 = [];
    therm.forEach(elem => {
        prints2.push(util.types.isNativeError(elem) ? util.inspect(elem, false, 1, false) : util.inspect(elem, false, null, false));
    });
    console.error(time, errId, prints1, prints2);
    return true;
};
/**
 * 
 * @param {*} errObj 主要错误内容
 * @param {ctx} c 上下文
 * @param  {...any} therm 其它附加内容
 */
const err_uri = (errObj, c, ...therm) => {
    return err(errObj, `${c.method} ${c.url}`, c, therm);
};

const _send = (c, opts) => {

    let body = {
        code: opts && opts.code || js.rescode.succress,
        data: opts && opts.data || ''
    };
    if (opts && opts.count !== null && opts.count !== undefined && (opts.count >= 0)) {
        body.count = opts.count;
    }

    c.body = body;
};
/** 向客户端发送
 * 
 * @param {ctx} c 
 * @param {*} data 要发回客户端的数据
 * @param {Number} 代码,应该从js.rescode中获取,默认js.rescode.succress
 */
const send = (c, data = '', code = js.rescode.succress) => {
    _send(c, {
        data,
        code
    });
};

/** 向客户端发送成功信息和数据
 * 
 * @param {ctx} c 
 * @param {*} data 要发回客户端的数据
 * @param {int} count 分页信息中的总记录条数
 */
const send_list = (c, data = [], count = -1) => {
    _send(c, {
        data,
        count
    });
};

/** 向客户端快速发送数组的第一个值, 若数组没有值, 仍然会发送成功
 * 
 * @summary 一般用于数据库查询结果的发送
 * @param {ctx} c 
 * @param {Array} data 要发送的原数组
 */
const send_shift = (c, data = []) => {
    let data_shift = {};
    if (data.length)
        data_shift = data[0];

    _send(c, {
        data: data_shift
    });
};


const got = require('got');
const sms = conf.sms;
/** 发送短信
 * 
 * @param {string} tel 短信号码
 * @param {string} msg 短信内容
 * @returns {Promise}
 * @returns null
 * @example
 * const pc = require('./projectCtl');
 * await pc.send_sms('13688004256','you are best');
 */
function send_sms(tel, msg) {
    return new Promise(async r => {

        await got(sms.url, {
            method: 'POST',
            body: JSON.stringify({
                "account": sms.account,
                "password": sms.password,
                "msg": sms.signkey + "" + msg,
                "phone": "" + tel,
                "report": "true"
            })
        });
        r();

    }); //Promise end
}



// 创建所有目录
const fs = require('fs');
const path = require('path');
/** 创建指定目录路径上的所有文件夹
 * 
 * @param {String} dirpath 要创建的路径
 * @param {*} mode 文件夹权限
 * @example
 * await mkdirs('public/upload/', 0777)
 */
function mkdirs(dirpath, mode = 0777) {
    return new Promise(async (r, e) => {
        try {
            let exists = fs.existsSync(dirpath);
            if (exists) {
                r(dirpath);
            } else {
                //尝试创建父目录，然后再创建当前目录
                await mkdirs(path.dirname(dirpath), mode);
                fs.mkdirSync(dirpath, mode);
                r(dirpath);
            }
        } catch (err) {
            e(err);
        }
    });
}

const Busboy = require('busboy');
/** 路由文件上传
 * 
 * @param {String} extendsions 扩展名限制
 * @param {String} filePath 保存(相对)路径
 * @returns {null}
 * @example
 * router.post('/upload', ph.save_file('pdf,doc,docx',`public/upload/`), async (c, n) => {
 *     console.log(c.upload_files); //文件上传结果
 *     console.log(c.upload_files_field);   //随文件上传的表单内容
 * })
 * 
 */
const save_file = (extendsions = '', filePath = '') => {
    return (c, n) => {
        return new Promise(async r => {

            //构建存储目录
            filePath = (filePath || `public/upload/`) + js.format_datetime(new Date(), `/yyyy/MM/dd/`);
            //去掉多余的斜杠
            filePath = filePath.replace(`//`, `/`);
            //创建文件夹
            await mkdirs(filePath, 0777);

            //构建上传结果
            c.upload_files = []; //上传后的文件结果
            c.upload_files_field = {}; //上传时同步提交的表单内容

            let busboy = new Busboy({
                headers: c.req.headers
            });
            busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
                file.on('data', function () { }); //让file开始工作
                //获取文件后缀
                let filetype = (filename).split('.').pop().toLowerCase();
                //如果没有文件类型||(有后缀要求&&后缀不在允许表中)
                if (!filetype || (extendsions && extendsions.toLowerCase().indexOf(filetype) < 0)) {
                    c.upload_files.push({
                        code: js.rescode.file_fail,
                        msg: filename + '不允许的文件类型'
                    });
                    return;
                }

                //构建新的文件名,进来保障不重复
                let newfilename = Math.random().toString(6).substring(2) + Date.now() + '.' + filetype;
                //创建写入流
                let fstream = fs.createWriteStream(filePath + newfilename);
                //写入文件
                file.pipe(fstream);
                file.on('end', function () {
                    c.upload_files.push({
                        code: js.rescode.succress,
                        msg: '上传成功',
                        data: {
                            imgPath: `${filePath}${newfilename}`,
                            imgKey: newfilename
                        }
                    });
                });
            });
            busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated) {
                c.upload_files_field[fieldname] = val;
            });
            busboy.on('finish', function () {
                r();
                n();
            });
            c.req.pipe(busboy);

        }); //Promise end    
    }; //c,n end
};

/** 保存图片
 * 
 * @param {String} filePath 保存路径
 */
const save_img = (c, n) => {
    return new Promise(async r => {
        r((await save_file('png,jpeg,jpg,jpe,jfif,gif,bmp,dib,tif,tiff,heic,ico,icon'))(c, n));
    });
};


const formidable = require('formidable');
const OSS = require('ali-oss');
/** 路由文件上传_阿里云
 * 
 * @param {String} extendsions 扩展名限制
 * @param {String} filePath 保存(相对)路径
 * @returns {null}
 * @example
 * router.post('/upload', ph.save_file('pdf,doc,docx',`public/upload/`), async (c, n) => {
 *     console.log(c.upload_files); //文件上传结果
 *     console.log(c.upload_files_field);   //随文件上传的表单内容
 * })
 * 
 */
const save_file_aliyun = (extendsions = '', filePath = '') => {
    return (c, n) => {
        return new Promise(async r => {

            //构建上传结果
            c.upload_files = []; //上传后的文件结果
            c.upload_files_field = {}; //上传时同步提交的表单内容

            //上传单文件，使用formidable
            let form = new formidable.IncomingForm();
            form.parse(c.req, function (err, fields, files) {
                if (err) {
                    c.upload_files.push({
                        code: js.rescode.file_fail,
                        msg: err
                    });
                    r();
                    n();
                    return;
                }

                // 文件名
                let time = js.format_datetime(new Date(), 'yyyyMMdd');
                let filepath = time + '/' + Math.random().toString(6).substring(2) + Date.now();
                let fileext = files.file.name.split('.').pop();
                let upfile = files.file.path;
                let newfile = filepath + '.' + fileext;

                //如果没有文件类型||(有后缀要求&&后缀不在允许表中)
                if (!fileext || (extendsions && extendsions.toLowerCase().indexOf(fileext) < 0)) {
                    c.upload_files.push({
                        code: js.rescode.file_fail,
                        msg: filename + '不允许的文件类型'
                    });
                    r();
                    n();
                    return;
                }

                //ali-oss
                co(function* () {
                    var client = new OSS({
                        region: conf.aliyun.region, //<Your region>
                        accessKeyId: conf.aliyun.accessKeyId, //<Your AccessKeyId>
                        accessKeySecret: conf.aliyun.accessKeySecret //<Your AccessKeySecret>
                    });
                    client.useBucket(conf.aliyun.bucket);
                    let result = yield client.put(newfile, upfile);
                    c.upload_files.push({
                        code: js.rescode.succress,
                        msg: '上传成功',
                        data: {
                            imgPath: result.url,
                            imgKey: result.name.split('/').pop()
                        }
                    });

                    r();
                    n();
                }).catch(function (err) {
                    c.upload_files.push({
                        code: js.rescode.file_fail,
                        msg: err,
                        info: err.message
                    });
                    r();
                    n();
                    return;
                });
            });

        }); //Promise end    
    }; //c,n end
};

/** 保存图片_阿里云
 * 
 * @param {String} filePath 保存路径
 */
const save_img_aliyun = (c, n) => {
    return new Promise(async r => {
        r((await save_file_aliyun('png,jpeg,jpg,jpe,jfif,gif,bmp,dib,tif,tiff,heic,ico,icon'))(c, n));
    });
};

//生成与解析token,jsonwebtoken.sign生成的token,会被koa-jwt自动解析到ctx.state.user中
const jsonwebtoken = require('jsonwebtoken');
const {
    createCipher
} = require('crypto');
/** 初始化用户登录信息
 * 
 * @param {*} id 用户ID
 * @param {Number} rights 用户权限
 * @returns {String} 签名token,有效期1h
 * @example
 * 权限为与或计算,权限值为2开始的数字的平方:
 * 2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,
 * 32768,65536,131072,262144,524288,1048576,2097152,4194304
 * 增加权限: right |= 2;
 * 判断权限: auth_jwt(2);
 * 手写判断: (right & 2)===2;
 */
const auth_jwt_sign = (id, rights = 0) => {
    let timeout = 60 * 60; //1小时
    let token = jsonwebtoken.sign({
        id
    }, // 加密userToken
        conf.jwt_secret, {
        expiresIn: timeout
    });
    //配合store保存用户权限,这里的过期时间需要和jwt过期时间保持一致(或者增加30秒以避免误差)
    jwt_store.set(id, rights, timeout + 30);
    return token;
};
/** 尝试刷新jwt令牌
 * @param {ctx} c 上下文
 * @param {bool} force 强制刷新
 *  @returns {String} 签名token,有效期1h
 */
const auth_jwt_refresh = (c, force = false) => {
    if (!c.state || !c.state.user)
        js.throwObj('用户未登录', js.rescode.login_fail);

    //如果被要求强制刷新,或者token剩余时间小于阈值,则更新token后返回
    if (force || c.state.user.exp - Date.parse(new Date()) / 1000 < conf.jwt_refresh_threshold) {
        return auth_jwt_sign(c.state.user.id, c.state.user.rights);
    }
    //否则返回当前token
    else {
        return c.header.authorization;
    }
};
/** 验证用户权限或登录状态
 * 
 * @param {Number} right 要验证的权限
 */
const auth_jwt = (right) => {
    return async (c, n) => {

        if (!c.state || !c.state.user)
            js.throwObj('用户未登录', js.rescode.login_fail);

        let rights = jwt_store.get(c.state.user.id);
        if ((rights & right) != right)
            js.throwObj('没有权限', js.rescode.auth_fail);

        //验证通过
        await n();

    }; //c,n end
};


/** 初始化用户登录信息(session方式)
 * 
 * @param {ctx} c 上下文
 * @param {*} id 用户ID
 * @param {Number} rights 用户权限
 * @example
 * 权限为与或计算,权限值为2开始的数字的平方:
 * 2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,
 * 32768,65536,131072,262144,524288,1048576,2097152,4194304
 * 增加权限: right |= 2;
 * 判断权限: auth_sess(2);
 * 手写判断: (right & 2)===2;
 */
const auth_sess_init = (c, id, rights) => {
    c.session.user = {
        id,
        rights
    };
};
/** 验证用户权限或登录状态
 * 
 * @param {Number} right 要验证的权限
 */
const auth_sess = (right) => {
    return async (c, n) => {

        if (!c.session || !c.session.user)
            js.throwObj('用户未登录', js.rescode.login_fail);

        let rights = c.session.user.rights;
        if ((rights & right) != right)
            js.throwObj('没有权限', js.rescode.auth_fail);

        //验证通过
        await n();

    }; //c,n end
};


module.exports = {
    store,
    callbackify,
    promisify,
    getSystemErrorName,
    log,
    err,
    err_uri,
    send,
    send_list,
    send_shift,
    send_sms,
    mkdirs,
    save_file,
    save_img,
    save_file_aliyun,
    save_img_aliyun,
    auth_sess_init,
    auth_sess,
    auth_jwt_sign,
    auth_jwt_refresh,
    auth_jwt,
};